package devcken.taglibrary;

import java.io.IOException;
import java.util.Collection;
import java.util.Locale;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.BodyTagSupport;

import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.LocaleResolver;

/**
 * 열거형 태그를 이용해 HTML Input 태그를 렌더링하기 위한 태그 핸들러로 {@link javax.servlet.jsp.tagext.BodyTagSupport BodyTabSupport}를 확장한다.
 * Input 태그를 만들기 위해 XML 문서 도구를 이용한다.
 * @author Devcken
 */
public class EnumerationInputTag extends BodyTagSupport
{
	private static final long serialVersionUID = 2390370729997701632L;
	
	private static final Logger logger = LoggerFactory.getLogger(EnumerationTagLoader.class);
	
	/**
	 * Input 태그 식별을 위한 상수 목록으로 switch 문 사용을 위해 해쉬 코드를 이용한다.
	 */
	private final int _INPUT_TYPE_SELECT = -906021636;
	private final int _INPUT_TYPE_LIST = 3322014;
	private final int _INPUT_TYPE_CHECK = 94627080;
	private final int _INPUT_TYPE_RADIO = 108270587;
	private final int _INPUT_TYPE_UL = 3735;
	private final int _INPUT_TYPE_BUTTON = -1377687758;
	
	/**
	 * 열거형 태그 관리자로 Input 태그를 위한 열거형 태그를 가져오기 위해 사용된다.
	 */
	private EnumerationTagLoader _enumerationTagLoader;
	
	/**
	 * i18n 메시지 관리자를 나타낸다.
	 */
	private MessageSource _messageSource;
	
	/**
	 * HTTP 요청 헤더에 해당되는 지역 정보를 나타낸다.
	 */
	private Locale _currentLocale;
	
	/**
	 * 어플리케이션 컨텐스트를 나타낸다.
	 */
	private ApplicationContext _context;
	
	/**
	 * 열거형 태그의 명칭으로 일치하는 열거형 태그를 가져오기 위해 반드시 설정되어야 한다.
	 */
	private String code;
	
	/**
	 * 렌더링하고자 하는 Input 태그의 타입으로 실제 HTML 태그명으로 반드시 설정되어야 한다.
	 */
	private String input;
	
	/**
	 * Input 태그의 이름으로 반드시 설정되어야 한다.
	 */
	private String name;
	
	/**
	 * Input 태그에서 초기 선택을 위한 값을 나타낸다. 필수 사항은 아니다.
	 */
	private String selected;
	
	/**
	 * Input 태그에 CSS을 설정하기 위한 값(class 속성)을 나타낸다. 필수 사항은 아니다.
	 */
	private String style;
	
	/**
	 * Input 태그에서 선택 값을 변경했을 때 발생하는 자바스크립트 함수를 설정할 수 있다. 필수 사항은 아니다. 
	 */
	private String change;
	
	/**
	 * 열거형 태그의 상수 속성 중 첫번째 그룹에 해당되는 상수만 가져오고 싶을 때 'true'로 설정한다. 기본 값은 'false'이다.
	 */
	private boolean group1;
	
	/**
	 * 열거형 태그의 상수 속성 중 두번째 그룹에 해당되는 상수만 가져오고 싶을 때 'true'로 설정한다. 기본 값은 'false'이다.
	 */
	private boolean group2;
	
	/**
	 * 열거형 태그의 상수 속성 중 세번째 그룹에 해당되는 상수만 가져오고 싶을 때 'true'로 설정한다. 기본 값은 'false'이다.
	 */
	private boolean group3;
	
	/**
	 * Input 태그 유형에 맞는 Input 태그 문자열을 생성한다.
	 * @return 열거형 태그에 맞는 Input 태그 문자열을 반환한다.
	 */
	private String generateTag()
	{
		EnumerationTag tag = this._enumerationTagLoader.getTag(this.code);
		
		String result = "";
		
		// Input 태그에 따라 태그 문자열을 만든다.
		switch (this.input.hashCode())
		{
			case _INPUT_TYPE_SELECT:
				
				result = this.generateSelectInput(tag);
				
				break;
			
			case _INPUT_TYPE_LIST:
				
				result = this.generateListInput(tag);
				
				break;
				
			case _INPUT_TYPE_CHECK:
				
				result = this.generateCheckInput(tag);
				
				break;
				
			case _INPUT_TYPE_RADIO:
				
				result = this.generateRadioInput(tag);
				
				break;
				
			case _INPUT_TYPE_UL:
				
				result = this.generateUlTag(tag);
				
				break;
				
			case _INPUT_TYPE_BUTTON:
				
				result = this.generateButtonTag(tag);
				
				break;
		}
		
		return result;
	}
	
	/**
	 * 셀렉트 박스 태그 문자열을 생성한다.
	 * @param tag 열거형 태그를 설정한다.
	 * @return 열거형 태그에 해당하는 셀렉트 박스 태그 문자열을 반환한다.
	 */
	private String generateSelectInput(EnumerationTag tag)
	{
		try
		{
			Document document = DocumentHelper.createDocument();
			
			Element select = document.addElement("select");
			
			select.addAttribute("name", this.name);
			select.addAttribute("id", this.name);
			
			if (!this.style.equals(""))
			{
				select.addAttribute("class", this.style);
			}
			
			if (!this.change.equals(""))
			{
				select.addAttribute("onchange", this.change);
			}
			
			this.generateOptionInput(tag, document);
	
			return select.asXML();
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
			
			return "";
		}
	}
	
	/**
	 * 리스트 박스 태그 문자열을 생성한다.
	 * @param tag 열거형 태그를 설정한다.
	 * @return 열거형 태그에 해당하는 리스트 박스 태그 문자열을 반환한다.
	 */
	private String generateListInput(EnumerationTag tag)
	{
		try
		{
			Document document = DocumentHelper.createDocument();
			
			Element select = document.addElement("select");
			
			select.addAttribute("name", this.name);
			select.addAttribute("id", this.name);
			select.addAttribute("multiple", "multiple");
			
			if (!this.style.equals(""))
			{
				select.addAttribute("class", this.style);
			}
			
			if (!this.change.equals(""))
			{
				select.addAttribute("onchange", this.change);
			}
			
			this.generateOptionInput(tag, document);
	
			return select.asXML();
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
			
			return "";
		}
	}
	
	/**
	 * 셀렉트 혹은 리스트 태그 XML 문서에 옵션 태그 XML을 생성하여 추가한다.
	 * 옵션 태그 문자열은 단독으로는 사용될 수 없으며, 셀렉트 혹은 리스트 박스 태그 요청 시에만 사용된다.
	 * @param tag 열거형 태그를 설정한다.
	 * @param document 셀렉트 태그나 리스트 태그의 XML 문서로 옵션 태그를 채워넣는다.
	 * @return 옵션 태그가 포함된 셀렉트 혹은 리스트 박스 태그 XML 문서를 반환한다.
	 */
	private Document generateOptionInput(EnumerationTag tag, Document document)
	{
		try
		{
			Collection<EnumerationTagAttribute> attributes = tag.getAttributes();
			
			for (EnumerationTagAttribute item : attributes)
			{
				if (this.group1 || this.group2 || this.group3)
				{
					if (!item.isGroup1() && !item.isGroup2() && !item.isGroup3())
					{
						continue;
					}
				}
				
				Element option = document.getRootElement() != null ? document.getRootElement().addElement("option") : document.addElement("option");
				
				option.addAttribute("value", item.getValue());
				
				if (this.selected != null && this.selected.equals(item.getValue()))
				{
					option.addAttribute("selected", "selected");
				}
				
				if (this.style != null && !this.style.equals(""))
				{
					option.addAttribute("class", this.style);
				}
				
				option.setText(this.convertAttributeNameToi18nMessage(item.getName()));
			}
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return document;
	}
	
	/**
	 * 체크 박스 태그 문자열을 생성한다.
	 * @param tag 열거형 태그를 설정한다.
	 * @return 열거형 태그에 해당하는 체크 박스 태그 문자열을 반환한다.
	 */
	private String generateCheckInput(EnumerationTag tag)
	{
		StringBuilder result = new StringBuilder();
		
		try
		{
			Document document = DocumentHelper.createDocument();
			
			Element root = document.addElement("root");
			
			Collection<EnumerationTagAttribute> attributes = tag.getAttributes();
			
			for (EnumerationTagAttribute item : attributes)
			{
				if (this.group1 || this.group2 || this.group3)
				{
					if (!item.isGroup1() && !item.isGroup2() && !item.isGroup3())
					{
						continue;
					}
				}
				
				Element label = root.addElement("label");
				
				label.addAttribute("style", "cursor: pointer;");
	
				Element check = label.addElement("input");
				
				check.addAttribute("type", "checkbox");
				check.addAttribute("name", this.name);
				check.addAttribute("value", item.getValue());
				check.addAttribute("style", "cursor: pointer;");
				
				if (this.selected != null && this.selected.equals(item.getValue()))
				{
					check.addAttribute("checked", "checked");
				}
				
				if (this.change != null && !this.change.equals(""))
				{
					check.addAttribute("onchange", this.change);
				}
				
				if (this.style != null && !this.style.equals(""))
				{
					check.addAttribute("class", this.style);
				}
				
				label.setText(this.convertAttributeNameToi18nMessage(item.getName()));
			}
			
			for (Object item : root.elements("label"))
			{
				Element element = (Element)item;
				
				result.append(element.asXML());
			}
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return result.toString();
	}
	
	/**
	 * 라디오 버튼 태그 문자열을 생성한다.
	 * @param tag 열거형 태그를 설정한다.
	 * @return 열거형 태그에 해당하는 라디오 버튼 태그 문자열을 반환한다.
	 */
	private String generateRadioInput(EnumerationTag tag)
	{
		StringBuilder result = new StringBuilder();
		
		try
		{
			Document document = DocumentHelper.createDocument();
			
			Element root = document.addElement("root");
			
			Collection<EnumerationTagAttribute> attributes = tag.getAttributes();
			
			for (EnumerationTagAttribute item : attributes)
			{
				if (this.group1 || this.group2 || this.group3)
				{
					if (!item.isGroup1() && !item.isGroup2() && !item.isGroup3())
					{
						continue;
					}
				}
				
				Element label = root.addElement("label");
				
				label.addAttribute("style", "cursor: pointer;");
				
				Element radio = label.addElement("input");
				
				radio.addAttribute("type", "radio");
				radio.addAttribute("name", this.name);
				radio.addAttribute("value", item.getValue());
				radio.addAttribute("style", "cursor: pointer;");
				
				if (this.selected != null && this.selected.equals(item.getValue()))
				{
					radio.addAttribute("checked", "checked");
				}
				
				if (this.change != null && !this.change.equals(""))
				{
					radio.addAttribute("onchange", this.change);
				}
				
				if (this.style != null && !this.style.equals(""))
				{
					radio.addAttribute("class", this.style);
				}
				
				label.setText(convertAttributeNameToi18nMessage(item.getName()));
			}
			
			for (Object item : root.elements("label"))
			{
				Element element = (Element)item;
				
				result.append(element.asXML());
			}
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return result.toString();
	}
	
	/**
	 * UL 태그 문자열을 생성한다.
	 * @param tag 열거형 태그를 설정한다.
	 * @return 열거형 태그에 해당하는 UL 태그 문자열을 반환한다.
	 */
	private String generateUlTag(EnumerationTag tag)
	{
		StringBuilder result = new StringBuilder();
		
		try
		{
			Document document = DocumentHelper.createDocument();
			
			Element root = document.addElement("root");
			
			Collection<EnumerationTagAttribute> attributes = tag.getAttributes();
			
			Element ul = root.addElement("ul");
			
			ul.addAttribute("id", this.name);
			
			for (EnumerationTagAttribute item : attributes)
			{
				if (this.group1 || this.group2 || this.group3)
				{
					if (!item.isGroup1() && !item.isGroup2() && !item.isGroup3())
					{
						continue;
					}
				}
				
				Element li = ul.addElement("li");
				
				li.addAttribute("id", item.getName());
				li.addAttribute("value", item.getValue());
				
				li.setText(convertAttributeNameToi18nMessage(item.getName()));
			}
			
			for (Object item : root.elements("ul"))
			{
				Element element = (Element)item;
				
				result.append(element.asXML());
			}
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return result.toString();
	}
	
	/**
	 * 버튼 태그 문자열을 생성한다.
	 * @param tag 열거형 태그를 설정한다.
	 * @return 열거형 태그에 해당하는 버튼 태그 문자열을 반환한다.
	 */
	private String generateButtonTag(EnumerationTag tag)
	{
		StringBuilder result = new StringBuilder();
		
		try
		{
			Document document = DocumentHelper.createDocument();
			
			Element root = document.addElement("root");
			
			Collection<EnumerationTagAttribute> attributes = tag.getAttributes();
			
			Element container = root.addElement("div");
			
			container.addAttribute("id", this.name);
			
			for (EnumerationTagAttribute item : attributes)
			{
				if (this.group1 || this.group2 || this.group3)
				{
					if (!item.isGroup1() && !item.isGroup2() && !item.isGroup3())
					{
						continue;
					}
				}
				
				Element button = container.addElement("button");
				
				button.addAttribute("id", item.getName());
				button.addAttribute("value", item.getValue());
				
				button.setText(convertAttributeNameToi18nMessage(item.getName()));
			}
			
			for (Object item : root.elements("div"))
			{
				Element element = (Element)item;
				
				result.append(element.asXML());
			}
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return result.toString();
	}
	
	/**
	 * 열거형 태그의 상수 명칭에 해당하는 i18n 메시지를 가져온다.
	 * @param code 열거형 태그의 상수 명칭을 설정한다.
	 * @return 열거형 태그의 상수 명칭에 해당하는 i18n 메시지를 반환한다. 열거형 태그의 상수 명칭에 해당하는 i18n 메시지가 존재하지 않는 경우, 상수 명칭을 반환한다.
	 */
	private String convertAttributeNameToi18nMessage(String code)
	{
		try
		{
			return this._messageSource.getMessage(code, null, this._currentLocale);
		}
		catch (NoSuchMessageException error)
		{
			return code;
		}
	}

	/**
	 * HTML 태그 렌더링을 준비한다.
	 */
	@Override
	public int doStartTag() throws JspException
	{
		try
		{
			// 어플리케이션 컨텍스트를 가져온다.
			this._context = WebApplicationContextUtils.getWebApplicationContext(this.pageContext.getServletContext());
			
			// 열거형 태그 로더 bean을 가져온다. bean이 존재하지 않을 경우 예외가 발생한다.
			this._enumerationTagLoader = this._context.getBean(EnumerationTagLoader.class);
			
			// 지역화 정보 관리자 bean을 가져온다. bean이 존재하지 않을 경우 예외가 발생한다.
			LocaleResolver localeResolver = this._context.getBean(LocaleResolver.class);
			
			// i18n 메시지를 가져오기 위해 현재의 Locale 설정을 가져온다.
			this._currentLocale = localeResolver.resolveLocale((HttpServletRequest)pageContext.getRequest());
			
			this._messageSource = this._context.getBean(MessageSource.class);
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return super.doStartTag();
	}

	/**
	 * HTML 태그를 만들어 페이지에 쓴다.
	 */
	@Override
	public int doEndTag() throws JspException
	{
		if (this._enumerationTagLoader == null)
		{
			logger.warn("Tag genration is impossible.");
			
			return EVAL_PAGE;
		}
		
		try
		{
			// HTML 태그를 만든다.
			String tag = this.generateTag();
			
			JspWriter writer = pageContext.getOut();
			
			writer.print(tag);
			writer.flush();
		}
		catch (IOException error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return EVAL_PAGE;
	}

	public String getCode()
	{
		return code;
	}

	public void setCode(String code)
	{
		this.code = code;
	}

	public String getInput()
	{
		return input;
	}

	public void setInput(String input)
	{
		this.input = input;
	}

	public String getName()
	{
		return name;
	}

	public void setName(String name)
	{
		this.name = name;
	}

	public String getSelected()
	{
		return selected;
	}

	public void setSelected(String selected)
	{
		this.selected = selected;
	}

	public String getStyle()
	{
		return style;
	}

	public void setStyle(String style)
	{
		this.style = style;
	}

	public String getChange()
	{
		return change;
	}

	public void setChange(String change)
	{
		this.change = change;
	}

	public boolean isGroup1()
	{
		return group1;
	}

	public void setGroup1(boolean group1)
	{
		this.group1 = group1;
	}

	public boolean isGroup2()
	{
		return group2;
	}

	public void setGroup2(boolean group2)
	{
		this.group2 = group2;
	}

	public boolean isGroup3()
	{
		return group3;
	}

	public void setGroup3(boolean group3)
	{
		this.group3 = group3;
	}
}